Merge pull request #1086 from azati/master

Add Beeper agent

Andrew Cantino 9 years ago
parent
commit
099aae6111
2 changed files with 273 additions and 0 deletions
  1. 128 0
      app/models/agents/beeper_agent.rb
  2. 145 0
      spec/models/agents/beeper_agent_spec.rb

+ 128 - 0
app/models/agents/beeper_agent.rb

@@ -0,0 +1,128 @@
1
+module Agents
2
+  class BeeperAgent < Agent
3
+    cannot_be_scheduled!
4
+    cannot_create_events!
5
+
6
+    description <<-MD
7
+      Beeper agent sends messages to Beeper app on your mobile device via Push notifications.
8
+
9
+      You need a Beeper Application ID (`app_id`), Beeper REST API Key (`api_key`) and Beeper Sender ID (`sender_id`) [https://beeper.io](https://beeper.io)
10
+
11
+      You have to provide phone number (`phone`) of the recipient which have a mobile device with Beeper installed, or a `group_id` – Beeper Group ID
12
+
13
+      Also you have to provide a message `type` which has to be `message`, `image`, `event`, `location` or `task`.
14
+
15
+      Depending on message type you have to provide additional fields:
16
+
17
+      ##### Message
18
+      * `text` – **required**
19
+
20
+      ##### Image
21
+      * `image` – **required** (Image URL or Base64-encoded image)
22
+      * `text` – optional
23
+
24
+      ##### Event
25
+      * `text` – **required**
26
+      * `start_time` – **required** (Corresponding to ISO 8601)
27
+      * `end_time` – optional (Corresponding to ISO 8601)
28
+
29
+      ##### Location
30
+      * `latitude` – **required**
31
+      * `longitude` – **required**
32
+      * `text` – optional
33
+
34
+      ##### Task
35
+      * `text` – **required**
36
+
37
+      You can see additional documentation at [Beeper website](https://beeper.io/docs)
38
+    MD
39
+
40
+    BASE_URL = 'https://api.beeper.io/api'
41
+
42
+    TYPE_ATTRIBUTES = {
43
+      'message'  => %w(text),
44
+      'image'    => %w(text image),
45
+      'event'    => %w(text start_time end_time),
46
+      'location' => %w(text latitude longitude),
47
+      'task'     => %w(text)
48
+    }
49
+
50
+    MESSAGE_TYPES = TYPE_ATTRIBUTES.keys
51
+
52
+    TYPE_REQUIRED_ATTRIBUTES = {
53
+      'message'  => %w(text),
54
+      'image'    => %w(image),
55
+      'event'    => %w(text start_time),
56
+      'location' => %w(latitude longitude),
57
+      'task'     => %w(text)
58
+    }
59
+
60
+    def default_options
61
+      {
62
+        'type'      => 'message',
63
+        'app_id'    => '',
64
+        'api_key'   => '',
65
+        'sender_id' => '',
66
+        'phone'     => '',
67
+        'text'      => '{{title}}'
68
+      }
69
+    end
70
+
71
+    def validate_options
72
+      %w(app_id api_key sender_id type).each do |attr|
73
+        errors.add(:base, "you need to specify a #{attr}") if options[attr].blank?
74
+      end
75
+
76
+      if options['type'].in?(MESSAGE_TYPES)
77
+        required_attributes = TYPE_REQUIRED_ATTRIBUTES[options['type']]
78
+        if required_attributes.any? { |attr| options[attr].blank? }
79
+          errors.add(:base, "you need to specify a #{required_attributes.join(', ')}")
80
+        end
81
+      else
82
+        errors.add(:base, 'you need to specify a valid message type')
83
+      end
84
+
85
+      unless options['group_id'].blank? ^ options['phone'].blank?
86
+        errors.add(:base, 'you need to specify a phone or group_id')
87
+      end
88
+    end
89
+
90
+    def working?
91
+      received_event_without_error? && !recent_error_logs?
92
+    end
93
+
94
+    def receive(incoming_events)
95
+      incoming_events.each do |event|
96
+        send_message(event)
97
+      end
98
+    end
99
+
100
+    def send_message(event)
101
+      mo = interpolated(event)
102
+      begin
103
+        response = HTTParty.post(endpoint_for(mo['type']), body: payload_for(mo), headers: headers)
104
+        error(response.body) if response.code != 201
105
+      rescue HTTParty::Error => e
106
+        error(e.message)
107
+      end
108
+    end
109
+
110
+    private
111
+
112
+    def headers
113
+      {
114
+        'X-Beeper-Application-Id' => options['app_id'],
115
+        'X-Beeper-REST-API-Key'   => options['api_key'],
116
+        'Content-Type' => 'application/json'
117
+      }
118
+    end
119
+
120
+    def payload_for(mo)
121
+      mo.slice(*TYPE_ATTRIBUTES[mo['type']], 'sender_id', 'phone', 'group_id').to_json
122
+    end
123
+
124
+    def endpoint_for(type)
125
+      "#{BASE_URL}/#{type}s.json"
126
+    end
127
+  end
128
+end

+ 145 - 0
spec/models/agents/beeper_agent_spec.rb

@@ -0,0 +1,145 @@
1
+require 'spec_helper'
2
+
3
+
4
+describe Agents::BeeperAgent do
5
+  let(:base_params) {
6
+    {
7
+      'type'      => 'message',
8
+      'app_id'    => 'some-app-id',
9
+      'api_key'   => 'some-api-key',
10
+      'sender_id' => 'sender-id',
11
+      'phone'     => '+111111111111',
12
+      'text'      => 'Some text'
13
+    }
14
+  }
15
+
16
+  subject {
17
+    agent = described_class.new(name: 'beeper-agent', options: base_params)
18
+    agent.user = users(:jane)
19
+    agent.save! and return agent
20
+  }
21
+
22
+  context 'validation' do
23
+    it 'valid' do
24
+      expect(subject).to be_valid
25
+    end
26
+
27
+    [:type, :app_id, :api_key, :sender_id].each do |attr|
28
+      it "invalid without #{attr}" do
29
+        subject.options[attr] = nil
30
+        expect(subject).not_to be_valid
31
+      end
32
+    end
33
+
34
+    it 'invalid with group_id and phone' do
35
+      subject.options['group_id'] ='some-group-id'
36
+      expect(subject).not_to be_valid
37
+    end
38
+
39
+    context '#message' do
40
+      it 'requires text' do
41
+        subject.options[:text] = nil
42
+        expect(subject).not_to be_valid
43
+      end
44
+    end
45
+
46
+    context '#image' do
47
+      before(:each) do
48
+        subject.options[:type] = 'image'
49
+      end
50
+
51
+      it 'invalid without image' do
52
+        expect(subject).not_to be_valid
53
+      end
54
+
55
+      it 'valid with image' do
56
+        subject.options[:image] = 'some-url'
57
+        expect(subject).to be_valid
58
+      end
59
+    end
60
+
61
+    context '#event' do
62
+      before(:each) do
63
+        subject.options[:type] = 'event'
64
+      end
65
+
66
+      it 'invalid without start_time' do
67
+        expect(subject).not_to be_valid
68
+      end
69
+
70
+      it 'valid with start_time' do
71
+        subject.options[:start_time] = Time.now
72
+        expect(subject).to be_valid
73
+      end
74
+    end
75
+
76
+    context '#location' do
77
+      before(:each) do
78
+        subject.options[:type] = 'location'
79
+      end
80
+
81
+      it 'invalid without latitude and longitude' do
82
+        expect(subject).not_to be_valid
83
+      end
84
+
85
+      it 'valid with latitude and longitude' do
86
+        subject.options[:latitude] = 15.0
87
+        subject.options[:longitude] = 16.0
88
+        expect(subject).to be_valid
89
+      end
90
+    end
91
+
92
+    context '#task' do
93
+      before(:each) do
94
+        subject.options[:type] = 'task'
95
+      end
96
+
97
+      it 'valid with text' do
98
+        expect(subject).to be_valid
99
+      end
100
+    end
101
+  end
102
+
103
+  context 'payload_for' do
104
+    it 'removes unwanted attributes' do
105
+      result = subject.send(:payload_for, {'type' => 'message', 'text' => 'text',
106
+        'sender_id' => 'sender', 'phone' => '+1', 'random_attribute' => 'unwanted'})
107
+      expect(result).to eq('{"text":"text","sender_id":"sender","phone":"+1"}')
108
+    end
109
+  end
110
+
111
+  context 'headers' do
112
+    it 'sets X-Beeper-Application-Id header with app_id' do
113
+      expect(subject.send(:headers)['X-Beeper-Application-Id']).to eq(base_params['app_id'])
114
+    end
115
+
116
+    it 'sets X-Beeper-REST-API-Key header with api_key' do
117
+      expect(subject.send(:headers)['X-Beeper-REST-API-Key']).to eq(base_params['api_key'])
118
+    end
119
+
120
+    it 'sets Content-Type' do
121
+      expect(subject.send(:headers)['Content-Type']).to eq('application/json')
122
+    end
123
+  end
124
+
125
+  context 'endpoint_for' do
126
+    it 'returns valid URL for message' do
127
+      expect(subject.send(:endpoint_for, 'message')).to eq('https://api.beeper.io/api/messages.json')
128
+    end
129
+
130
+    it 'returns valid URL for image' do
131
+      expect(subject.send(:endpoint_for, 'image')).to eq('https://api.beeper.io/api/images.json')
132
+    end
133
+
134
+    it 'returns valid URL for event' do
135
+      expect(subject.send(:endpoint_for, 'event')).to eq('https://api.beeper.io/api/events.json')
136
+    end
137
+
138
+    it 'returns valid URL for location' do
139
+      expect(subject.send(:endpoint_for, 'location')).to eq('https://api.beeper.io/api/locations.json')
140
+    end
141
+    it 'returns valid URL for task' do
142
+      expect(subject.send(:endpoint_for, 'task')).to eq('https://api.beeper.io/api/tasks.json')
143
+    end
144
+  end
145
+end